﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using BobcatWiiLib;
using BobcatUtility;

namespace Sample2_DraggingAndZooming
{
    /// <summary>
    /// User control which shows a picture and lets you drag it and zoom it using LED pens.
    /// </summary><remarks>
    /// We subscribe to IR source events from the CalibratedIRManager. If one source is detected 
    /// and it moves we treat this as a drag event. If two sources are detected and they move we
    /// treat this as a zoom event.
    /// 
    /// Note  : We calculate the new position of the picture control in response to events
    ///         coming from the IR sources, but we update the picture's bounds on a timer.
    ///         This is because moving the picture in the PictureBox control is rather slow,
    ///         so we cannot update it for every movement of the pen without slowing the
    ///         app too much.
    /// 
    /// </remarks>
    public partial class DraggablePicture : UserControl
    {
        #region Public methods

        /// <summary>
        /// Constructor.
        /// </summary>
        public DraggablePicture()
        {
            InitializeComponent();
        }


        /// <summary>
        /// You pass in the CalibratedIRManager that will provide IR state-change
        /// and movement events.
        /// </summary>
        /// <param name="irManager"></param>
        public void setCalibratedIRManager(CalibratedIRManager irManager)
        {
            // We register for events from the IR manager...
            m_IRManager = irManager;
            irManager.IRStateChangedEvent += onIRStateChanged;
            irManager.IRPositionChangedEvent += onIRPositionChanged;
        }


        /// <summary>
        /// Loads the image from the filename passed in.
        /// </summary>
        public void loadImageFromFile(string strFilename)
        {
            Image img = Image.FromFile(strFilename);
            Width = img.Width;
            Height = img.Height;
            ctrlPictureBox.Image = img;
        }

        #endregion

        #region Private helper functions

        /// <summary>
        /// Called when an IR source changes state, i.e. is turned on or off. 
        /// </summary>
        private void onIRStateChanged(object sender, CalibratedIRManager.IRArgs args)
        {
            // A IR source has been switched on or off. We decide what to do depending
            // on how many sources are visible. We want to know which sources are active 
            // _on this control_ so we first need to find the control's coordinates in
            // screen coordinates...
            Rectangle bounds = getBoundsInScreenCoordinates();
            IList<CalibratedIRManager.IRArgs> sourceInfos = m_IRManager.getActiveSources(bounds);

            // If we've been clicked on, we move our picture to the front...
            if (sourceInfos.Count > 0)
            {
                BringToFront();
            }

            switch (sourceInfos.Count)
            {
                case 0:
                    // There are no sources, so we don't take any action...
                    m_eMode = Mode.NONE;
                    break;

                case 1:
                    // One source is visible, so we want to start dragging.
                    m_eMode = Mode.DRAG;

                    // We store the start position of the source and the start 
                    // position of the control...
                    m_IRDragStartPoint = Utility.copy(sourceInfos[0].ScreenPoint);
                    m_ControlDragStartLeft = Left;
                    m_ControlDragStartTop = Top;
                    break;

                case 2:
                    // Two sources are visible, so we start a zoom. 
                    m_eMode = Mode.ZOOM;

                    // We find the initial distance between the two sources...
                    Point screenPoint0 = sourceInfos[0].ScreenPoint;
                    Point screenPoint1 = sourceInfos[1].ScreenPoint;
                    m_IRZoomStartDistance = getDistance(screenPoint0, screenPoint1);

                    m_ControlZoomStartWidth = Width;
                    m_ControlZoomStartHeight = Height;
                    m_ControlZoomStartLeft = Left;
                    m_ControlZoomStartTop = Top;

                    // We also need to record the start point of the topmost of the sources, so that
                    // we can try to keep the point over the same part of the picture...
                    CalibratedIRManager.IRArgs topmostPointInfo = (screenPoint0.Y < screenPoint1.Y) ? sourceInfos[0] : sourceInfos[1];
                    m_ZoomTopmostStartPoint_Screen = Utility.copy(topmostPointInfo.ScreenPoint);
                    m_ZoomTopmostStartPoint_Client = PointToClient(topmostPointInfo.ScreenPoint);
                    m_ZoomTopmostStartIndex = topmostPointInfo.Source;
                    break;
            }
        }


        /// <summary>
        /// Called when the position of an IR source has changed.
        /// </summary>
        private void onIRPositionChanged(object sender, CalibratedIRManager.IRArgs args)
        {
            // An IR source has moved, what we do depends on which mode we are in...
            switch (m_eMode)
            {
                case Mode.DRAG:
                    // We see where the point is now, and work out where the control ought to be...
                    int iXOffset = args.ScreenPoint.X - m_IRDragStartPoint.X;
                    int iYOffset = args.ScreenPoint.Y - m_IRDragStartPoint.Y;
                    m_iLeft = m_ControlDragStartLeft + iXOffset;
                    m_iTop = m_ControlDragStartTop + iYOffset;
                    m_iHeight = Height;
                    m_iWidth = Width;
                    m_bDataChanged = true;
                    break;

                case Mode.ZOOM:
                    // We find the current distance between the source points...
                    IList<CalibratedIRManager.IRArgs> sourceInfos = m_IRManager.getActiveSources();
                    if (sourceInfos.Count != 2)
                    {
                        return;
                    }
                    double dDistance = getDistance(sourceInfos[0].ScreenPoint, sourceInfos[1].ScreenPoint);
                    double dRatio = dDistance / m_IRZoomStartDistance;

                    // We work out the new size of the control...
                    double dWidth = m_ControlZoomStartWidth * dRatio;
                    double dHeight = m_ControlZoomStartHeight * dRatio;
                    m_iWidth = (int)(Math.Round(dWidth));
                    m_iHeight = (int)(Math.Round(dHeight));

                    // We work out the position so that the topmost IR source stays over the same
                    // part of the picture. We move the top-left of the control to compensate for 
                    // the zoom...
                    double dXOffset = (dRatio - 1.0) * m_ZoomTopmostStartPoint_Client.X;
                    double dYOffset = (dRatio - 1.0) * m_ZoomTopmostStartPoint_Client.Y;

                    // And to compensate for the movement of the topmost source...
                    Point topmostPoint = sourceInfos[m_ZoomTopmostStartIndex].ScreenPoint;
                    dXOffset += (m_ZoomTopmostStartPoint_Screen.X - topmostPoint.X);
                    dYOffset += (m_ZoomTopmostStartPoint_Screen.Y - topmostPoint.Y);

                    double dLeft = m_ControlZoomStartLeft - dXOffset;
                    double dTop = m_ControlZoomStartTop - dYOffset;
                    m_iLeft = (int)(Math.Round(dLeft));
                    m_iTop = (int)(Math.Round(dTop));

                    m_bDataChanged = true;
                    break;
            }
        }


        /// <summary>
        /// Converts the controls bounds to screen coordinates.
        /// </summary>
        Rectangle getBoundsInScreenCoordinates()
        {
            Rectangle result = new Rectangle();

            Point topLeft = PointToScreen(new Point(ClientRectangle.Left, ClientRectangle.Top));
            result.X = topLeft.X;
            result.Y = topLeft.Y;
            result.Width = Width;
            result.Height = Height;

            return result;
        }


        /// <summary>
        /// Returns the distance between two points.
        /// </summary>
        private double getDistance(Point a, Point b)
        {
            double dDeltaX = a.X - b.X;
            double dDeltaY = a.Y - b.Y;
            double dDistanceSquared = dDeltaX * dDeltaX + dDeltaY * dDeltaY;
            double dDistance = Math.Sqrt(dDistanceSquared);
            return dDistance;
        }


        /// <summary>
        /// Called at regular intervals by a timer. We check if there has been an update to the
        /// position or size of this control, and make the changes if necessary.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (false == m_bDataChanged)
            {
                // No changes have been made...
                return;
            }

            // Changes have been made, so we apply them...
            SetBounds(m_iLeft, m_iTop, m_iWidth, m_iHeight);
            m_bDataChanged = false;
        }

        #endregion

        #region Private data

        private CalibratedIRManager m_IRManager;

        private enum Mode
        {
            NONE,
            DRAG,
            ZOOM
        }
        private Mode m_eMode = Mode.NONE;

        // Drag information...
        private Point m_IRDragStartPoint = new Point();
        private int m_ControlDragStartLeft;
        private int m_ControlDragStartTop;

        // Zoom information...
        private Point m_ZoomTopmostStartPoint_Client = new Point();
        private Point m_ZoomTopmostStartPoint_Screen = new Point();
        private int m_ZoomTopmostStartIndex;
        private double m_IRZoomStartDistance;
        private int m_ControlZoomStartLeft;
        private int m_ControlZoomStartTop;
        private int m_ControlZoomStartWidth;
        private int m_ControlZoomStartHeight;

        // Thread-synchronized data holding control position and size...
        private bool m_bDataChanged = false;
        private int m_iTop;
        private int m_iLeft;
        private int m_iWidth;
        private int m_iHeight;

        #endregion

    }
}
